#!/usr/bin/env python3
# coding: utf-8
# Desc: 测试套公共模块
import abc
import json
import os.path
import re
import sys

from typing import List

sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
from tst_common.lib.common import TestCase, command_output


def get_arch():
    arch, _, _ = command_output('uname -m')
    return arch.strip('\n')


def get_kernel_version():
    ver, _, _ = command_output('uname -r')
    return ver.strip('\n')


def get_tk_release_type(kernel_version: str):
    if kernel_version.startswith('5.4.'):
        tk_version = 'tk4'
    elif kernel_version.startswith('4.14.'):
        tk_version = 'tk3'
    elif kernel_version.startswith('3.10.'):
        tk_version = 'tk2'
    else:
        raise ValueError(f'unknown version {kernel_version}')

    arch = get_arch()

    tk_release = 'public'
    if 'tlinux' in kernel_version:
        tk_release = 'private'

    if kernel_version.startswith('5.17.') or kernel_version.startswith('5.18.'):
        return f'tks-{arch}'
    else:
        return f'{tk_version}-{tk_release}-{arch}'


def get_host_tk_release_type():
    kernel_version, _, _ = command_output('uname -r')
    return get_tk_release_type(kernel_version)


class TKernelVersion:
    def __init__(self, version):
        self.version = version
        self.release_type = get_tk_release_type(self.version)
        match = re.match(r'^(\d+)\.(\d+)\.(\d+)-(\d+)-((tlinux\d).*-)?(\d+)?\.?(\d+)?(.*)?$',
                         self.version, re.IGNORECASE)
        if match is None:
            raise ValueError(f'the version {self.version} not match')
        self.kernel_ver_1 = int(match.groups()[0])
        self.kernel_ver_2 = int(match.groups()[1])
        self.kernel_ver_3 = int(match.groups()[2])
        self.tkernel_index = int(match.groups()[3])
        self.tlinux_type = '' if match.groups()[5] is None else match.groups()[5].strip('-_.')
        self.tkernel_version = 0 if match.groups()[6] is None else int(match.groups()[6])
        self.tkernel_subversion = 0 if match.groups()[7] is None else int(match.groups()[7])
        self.suffix = '' if match.groups()[8] is None else match.groups()[8]

    def to_string(self):
        version_dict = {
            'kernel_ver_1': self.kernel_ver_1,
            'kernel_ver_2': self.kernel_ver_2,
            'kernel_ver_3': self.kernel_ver_3,
            'tkernel_index': self.tkernel_index,
            'tlinux_type': self.tlinux_type,
            'tkernel_version': self.tkernel_version,
            'tkernel_subversion': self.tkernel_subversion,
            'suffix': self.suffix
        }
        return json.dumps(version_dict, indent=4)

    def is_same_tk_top_version(self, other) -> bool:
        if isinstance(other, str):
            other_object = TKernelVersion(other)
        elif isinstance(other, TKernelVersion):
            other_object = other
        else:
            raise TypeError(f'type {type(other)} can not compare with type {type(self)}')
        return (self.release_type == other_object.release_type) and (
                self.kernel_ver_1 == other_object.kernel_ver_1) and (
                       self.kernel_ver_2 == other_object.kernel_ver_2) and (
                       self.tlinux_type == other_object.tlinux_type)

    def is_same_tk_big_version(self, other, check_suffix: bool = True) -> bool:
        if isinstance(other, str):
            other_object = TKernelVersion(other)
        elif isinstance(other, TKernelVersion):
            other_object = other
        else:
            raise TypeError(f'type {type(other)} can not compare with type {type(self)}')
        if not self.is_same_tk_top_version(other_object):
            return False
        base_result = (self.kernel_ver_3 == other_object.kernel_ver_3) and (
                self.tkernel_index == other_object.tkernel_index) and (
                              self.tlinux_type == other_object.tlinux_type) and (
                              self.tkernel_version == other_object.tkernel_version)
        if check_suffix:
            return base_result and (self.suffix == other.suffix)
        else:
            return base_result

    def __str__(self):
        return self.version

    def print(self):
        attr_list = ['version', 'release_type', 'kernel_ver_1', 'kernel_ver_2', 'kernel_ver_3', 'tkernel_index',
                     'tlinux_type', 'tkernel_version', 'tkernel_subversion']
        for attr in attr_list:
            print(f'{attr:>20s} : {self.__getattribute__(attr)}')

    def __eq__(self, other):
        if isinstance(other, str):
            return self.version == other
        elif isinstance(other, TKernelVersion):
            return self.version == other.version
        else:
            raise TypeError(f'type {type(other)} can not compare with type {type(self)}')

    def __lt__(self, other):
        if isinstance(other, str):
            other_object = TKernelVersion(other)
        elif isinstance(other, TKernelVersion):
            other_object = other
        else:
            raise TypeError(f'type {type(other)} can not compare with type {type(self)}')
        # 版本比较时定义tk3比tk4小
        if self.kernel_ver_1 < other_object.kernel_ver_1:
            return True
        elif self.kernel_ver_1 > other_object.kernel_ver_1:
            return False

        if self.kernel_ver_2 < other_object.kernel_ver_2:
            return True
        elif self.kernel_ver_2 > other_object.kernel_ver_2:
            return False

        if self.kernel_ver_3 < other_object.kernel_ver_3:
            return True
        elif self.kernel_ver_3 > other_object.kernel_ver_3:
            return False

        if self.tkernel_index < other_object.tkernel_index:
            return True
        elif self.tkernel_index > other_object.tkernel_index:
            return False

        if self.tkernel_version < other_object.tkernel_version:
            return True
        elif self.tkernel_version > other_object.tkernel_version:
            return False

        if self.tkernel_subversion < other_object.tkernel_subversion:
            return True
        elif self.tkernel_subversion > other_object.tkernel_subversion:
            return False

        return False


class KABI:
    def __init__(self, kabi_line: str):
        self.kabi_line = kabi_line.strip().strip('\n').strip()
        line_split = self.kabi_line.split()
        self.crc = line_split[0]
        self.symbol = line_split[1]
        self.owner = line_split[2]
        self.type = line_split[3]

    def __str__(self):
        return f'{self.crc} {self.symbol} {self.owner} {self.type}'

    def __eq__(self, other):
        if isinstance(other, str):
            other_object = KABI(other)
        elif isinstance(other, KABI):
            other_object = other
        else:
            raise TypeError(f'type {type(other)} can not compare with type {type(self)}')
        return (self.crc == other_object.crc) and (self.symbol == other_object.symbol) and (
                self.owner == other_object.owner) and (self.type == other_object.type)


def kabi_file_to_tree(symbol_file):
    """
    将kabi文件转换成可以对比的字典
    :param symbol_file:
    :return: kabi内容，结构如下
                {
                    'vmlinux': {
                        'kmalloc': KABI,
                        'vmalloc': KABI,
                    },
                    'fs/nfs/nfs': {
                        'nfs_sb_active': KABI
                    }
                }
    """
    kabi_dict = dict()
    if not os.path.isfile(symbol_file):
        raise FileNotFoundError(f'the file {symbol_file} not exist')
    print(f'symbol_file: {symbol_file}')
    with open(symbol_file, 'r') as f:
        for line in f.readlines():
            kabi = KABI(line)
            if kabi.owner not in kabi_dict:
                kabi_dict[kabi.owner] = dict()
            if kabi.symbol in kabi_dict[kabi.owner]:
                raise KeyError(f'the symbol {kabi.symbol} of {kabi} has '
                               f'existed {kabi_dict[kabi.owner][kabi.symbol]}')
            kabi_dict[kabi.owner][kabi.symbol] = kabi
    return kabi_dict


def get_host_kabi_tree():
    version = get_kernel_version()
    symbol_file = os.path.join('/usr/src/kernels', version, 'Module.symvers')
    return kabi_file_to_tree(symbol_file)


def diff_kabi_tree(tree_left: dict, tree_right: dict):
    """
    比较两个kabi
    :param tree_left:
    :param tree_right:
    :return: kabi差异，结构如下
            {
                'same': {
                    'vmlinux': {
                        'vmalloc': KABI,
                    }
                },
                'diff': {
                    'vmlinux': {
                        'kmalloc': {
                            'left': KABI,
                            'right': KABI
                        },
                    },
                },
                'left': {
                    'vmlinux': {
                        'vmalloc': KABI,
                    }
                },
                'right': {
                    'vmlinux': {
                        'vmalloc': KABI,
                    }
                }
            }
    """
    diff_dict = {
        'same': dict(),
        'diff': dict(),
        'left': dict(),
        'right': dict()
    }
    for owner in tree_left:
        if owner not in tree_right:
            if owner in diff_dict['left']:
                raise Exception(f'{owner} only in left, diff["left"] can not exist')
            diff_dict['left'][owner] = tree_left[owner]
            continue
        for symbol in tree_left[owner]:
            kabi = tree_left[owner][symbol]
            if symbol not in tree_right[owner]:
                if owner not in diff_dict['left']:
                    diff_dict['left'][owner] = dict()
                if symbol in diff_dict['left'][owner]:
                    raise Exception(f'symbol {symbol} only in left, diff["left"][{owner}] can not exist')
                diff_dict['left'][owner][symbol] = kabi
            elif kabi != tree_right[owner][symbol]:
                if owner not in diff_dict['diff']:
                    diff_dict['diff'][owner] = dict()
                if symbol in diff_dict['diff'][owner]:
                    raise Exception(f'symbol {symbol} diff, but diff["diff"][{owner}] can not exist now')
                diff_dict['diff'][owner][symbol] = dict()
                diff_dict['diff'][owner][symbol]['left'] = kabi
                diff_dict['diff'][owner][symbol]['right'] = tree_right[owner][symbol]
            else:
                if owner not in diff_dict['same']:
                    diff_dict['same'][owner] = dict()
                if symbol in diff_dict['same'][owner]:
                    raise Exception(f'symbol {symbol} same, diff["same"][{owner}] can not exist now')
                diff_dict['same'][owner][symbol] = kabi
    for owner in tree_right:
        if owner not in tree_left:
            if owner in diff_dict['right']:
                raise Exception(f'{owner} only in right, diff["right"] can not exist')
            diff_dict['right'][owner] = tree_right[owner]
            continue
        for symbol in tree_right[owner]:
            kabi = tree_right[owner][symbol]
            if symbol not in tree_left[owner]:
                if owner not in diff_dict['right']:
                    diff_dict['right'][owner] = dict()
                if symbol in diff_dict['right'][owner]:
                    raise Exception(f'symbol {symbol} only in right, diff["right"][{owner}] can not exist')
                diff_dict['right'][owner][symbol] = kabi
            elif kabi != tree_left[owner][symbol]:
                if owner not in diff_dict['diff']:
                    diff_dict['diff'][owner] = dict()
                if symbol in diff_dict['diff'][owner]:
                    continue
                diff_dict['diff'][owner][symbol] = dict()
                diff_dict['diff'][owner][symbol]['left'] = kabi
                diff_dict['diff'][owner][symbol]['right'] = tree_right[owner][symbol]
            else:
                if owner not in diff_dict['same']:
                    diff_dict['same'][owner] = dict()
                if symbol in diff_dict['same'][owner]:
                    continue
                diff_dict['same'][owner][symbol] = kabi
    return diff_dict


def is_kabi_diff(diff_dict):
    # 如果只有新增的kabi，那么不算kabi变更
    for c in ['diff', 'left']:
        if (c in diff_dict) and (len(diff_dict[c]) > 0):
            return True
    return False


def print_kabi_diff(diff_dict: dict):
    print(f'Same  : {sum([len(diff_dict["same"][owner]) for owner in diff_dict["same"]])}')
    print(f'Diff  : {sum([len(diff_dict["diff"][owner]) for owner in diff_dict["diff"]])}')
    print(f'Left  : {sum([len(diff_dict["left"][owner]) for owner in diff_dict["left"]])}')
    print(f'Right : {sum([len(diff_dict["right"][owner]) for owner in diff_dict["right"]])}')
    for owner in sorted(diff_dict['diff'].keys()):
        for symbol in sorted(diff_dict['diff'][owner]):
            left = diff_dict['diff'][owner][symbol]['left']
            right = diff_dict['diff'][owner][symbol]['right']
            if left.type == right.type:
                print(f'D : {left.crc} <--> {right.crc} : : {symbol} : {owner}')
            else:
                print(f'D : {left.crc} <--> {right.crc} : {left.type} <--> {right.type} : {owner} : {symbol}')
    for owner in sorted(diff_dict['left'].keys()):
        for symbol in sorted(diff_dict['left'][owner]):
            left = diff_dict['left'][owner][symbol]
            print(f'L : {left.crc} : {left.type} : {owner} : {symbol}')
    for owner in sorted(diff_dict['right'].keys()):
        for symbol in sorted(diff_dict['right'][owner]):
            right = diff_dict['right'][owner][symbol]
            print(f'R :                 {right.crc} : {right.type} : {owner} {symbol}')


class MyTestCase(TestCase):
    """
    本测试套内的Python用例需要继承此类
    """

    def tc_setup_common(self, *args):
        """
        所有Python用例执行tc_setup函数前会先执行本函数
        :param args:
        :return:
        """
        self.msg("this is tc_setup_common")

    @abc.abstractmethod
    def do_test(self, *args):
        pass

    def tc_teardown_common(self, *args):
        """
        所有Python用例执行tc_teardown函数后会执行本函数
        :param args:
        :return:
        """
        self.msg("this is tc_teardown_common")

    def get_kabi_list(self, arch: str, version: str) -> List[KABI]:
        kabi_file = os.path.join(self.tst_ts_topdir, 'testcase', 'kabi', arch, f'{version}.Module.symvers')
        kabi_list = list()
        with open(kabi_file, 'r') as f:
            for line in f.readlines():
                kabi_list.append(KABI(line))
        return kabi_list

    def get_kabi_tree(self, arch: str, version: str) -> dict:
        """
        将KABI以树形展示
        :param arch: 架构
        :param version: 内核版本
        :return:
        """
        kabi_file = os.path.join(self.tst_ts_topdir, 'testcase', 'kabi', arch, f'{version}.Module.symvers')
        return kabi_file_to_tree(kabi_file)

    def diff_host_kabi(self):
        arch = get_arch()
        version = get_kernel_version()
        base_version = self.get_last_same_big_version(arch, version)
        if base_version is None:
            print(f'the base version not exist')
            return None
        return self.diff_kabi_tree_by_version(arch, str(base_version), version)

    def is_host_kabi_diff(self):
        diff_dict = self.diff_host_kabi()
        for c in ['diff', 'left', 'right']:
            if (c in diff_dict) and (len(diff_dict[c]) > 0):
                return True
        return False

    def is_kabi_diff_by_version(self, arch: str, left: str, right: str):
        tree_left = self.get_kabi_tree(arch, left)
        tree_right = self.get_kabi_tree(arch, right)
        return is_kabi_diff(diff_kabi_tree(tree_left, tree_right))

    def diff_kabi_tree_by_version(self, arch: str, left: str, right: str):
        tree_left = self.get_kabi_tree(arch, left)
        tree_right = self.get_kabi_tree(arch, right)
        return diff_kabi_tree(tree_left, tree_right)

    def get_sorted_kernel_version(self, arch: str) -> List[TKernelVersion]:
        version_list = list()
        self.msg(self.tst_ts_topdir)
        self.msg(arch)
        for f in os.listdir(os.path.join(self.tst_ts_topdir, 'testcase', 'kabi', arch)):
            version_list.append(TKernelVersion(f.replace('.Module.symvers', '')))
        version_list.sort()
        return version_list

    def get_last_same_big_version(self, arch: str, version: str):
        kernel_version = TKernelVersion(version)
        result = None
        for old in self.get_sorted_kernel_version(arch):
            if kernel_version.is_same_tk_big_version(old) and kernel_version != old:
                result = old
        return result
